package com.aabte.commons.redis.hash;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;

/**
 * @author Daniel
 * @version 1.0
 * @date 2020/3/21
 */
public class Murmur3_128Hasher {
  private static final int CHUNK_SIZE = 16;
  private static final long C1 = 0x87c37b91114253d5L;
  private static final long C2 = 0x4cf5ad432745937fL;
  /** Buffer via which we pass data to the hash algorithm (the implementor) */
  private final ByteBuffer buffer;

  /** Number of bytes to be filled before process() invocation(s). */
  private final int bufferSize;

  /** Number of bytes processed per process() invocation. */
  private final int chunkSize;

  private long h1;
  private long h2;
  private int length;

  public Murmur3_128Hasher(int seed) {
    this.h1 = seed;
    this.h2 = seed;
    this.length = 0;

    this.chunkSize = CHUNK_SIZE;
    this.bufferSize = CHUNK_SIZE;

    this.buffer = ByteBuffer.allocate(bufferSize + 7).order(ByteOrder.LITTLE_ENDIAN);
  }

  public final long hashAsLong() {
    munch();
    buffer.flip();
    if (buffer.remaining() > 0) {
      processRemaining(buffer);
      buffer.position(buffer.limit());
    }
    return makeHash();
  }

  private void processRemaining(ByteBuffer bb) {
    bb.position(bb.limit()); // move at the end
    bb.limit(chunkSize + 7); // get ready to pad with longs
    while (bb.position() < chunkSize) {
      bb.putLong(0);
    }
    bb.limit(chunkSize);
    bb.flip();
    process(bb);
  }

  private long makeHash() {
    h1 ^= length;
    h2 ^= length;

    h1 += h2;
    h2 += h1;

    h1 = fmix64(h1);
    h2 = fmix64(h2);

    h1 += h2;
    h2 += h1;

    byte[] bytes =
        ByteBuffer.wrap(new byte[CHUNK_SIZE])
            .order(ByteOrder.LITTLE_ENDIAN)
            .putLong(h1)
            .putLong(h2)
            .array();

    return padToLong(bytes);
  }

  private long padToLong(byte[] bytes) {
    long retVal = (bytes[0] & 0xFF);
    for (int i = 1; i < Math.min(bytes.length, 8); i++) {
      retVal |= (bytes[i] & 0xFFL) << (i * 8);
    }
    return retVal;
  }

  public Murmur3_128Hasher hashObject(CharSequence charSequence, Charset charset) {
    ByteBuffer buffer = ByteBuffer.allocate(1024 + 7).order(ByteOrder.LITTLE_ENDIAN);

    byte[] bytes = charSequence.toString().getBytes(charset);

    ByteBuffer readBuffer = ByteBuffer.wrap(bytes, 0, bytes.length).order(ByteOrder.LITTLE_ENDIAN);
    if (readBuffer.remaining() <= buffer.remaining()) {
      buffer.put(readBuffer);
      munchIfFull();
      return this;
    }

    // First add just enough to fill buffer size, and munch that
    int bytesToCopy = bufferSize - buffer.position();
    for (int i = 0; i < bytesToCopy; i++) {
      buffer.put(readBuffer.get());
    }
    munch(); // buffer becomes empty here, since chunkSize divides bufferSize

    // Now process directly from the rest of the input buffer
    while (readBuffer.remaining() >= chunkSize) {
      process(readBuffer);
    }

    // Finally stick the remainder back in our usual buffer
    buffer.put(readBuffer);
    return this;
  }

  // Process pent-up data in chunks
  private void munchIfFull() {
    if (buffer.remaining() < 8) {
      // buffer is full; not enough room for a primitive. We have at least one full chunk.
      munch();
    }
  }

  private void munch() {
    buffer.flip();
    while (buffer.remaining() >= chunkSize) {
      // we could limit the buffer to ensure process() does not read more than
      // chunkSize number of bytes, but we trust the implementations
      process(buffer);
    }
    buffer.compact(); // preserve any remaining data that do not make a full chunk
  }

  private void process(ByteBuffer bb) {
    long k1 = bb.getLong();
    long k2 = bb.getLong();
    bmix64(k1, k2);
    length += CHUNK_SIZE;
  }

  private void bmix64(long k1, long k2) {
    h1 ^= mixK1(k1);

    h1 = Long.rotateLeft(h1, 27);
    h1 += h2;
    h1 = h1 * 5 + 0x52dce729;

    h2 ^= mixK2(k2);

    h2 = Long.rotateLeft(h2, 31);
    h2 += h1;
    h2 = h2 * 5 + 0x38495ab5;
  }

  private static long fmix64(long k) {
    k ^= k >>> 33;
    k *= 0xff51afd7ed558ccdL;
    k ^= k >>> 33;
    k *= 0xc4ceb9fe1a85ec53L;
    k ^= k >>> 33;
    return k;
  }

  private static long mixK1(long k1) {
    k1 *= C1;
    k1 = Long.rotateLeft(k1, 31);
    k1 *= C2;
    return k1;
  }

  private static long mixK2(long k2) {
    k2 *= C2;
    k2 = Long.rotateLeft(k2, 33);
    k2 *= C1;
    return k2;
  }
}
